Author: Fernando
Cacciola
Contact: fernando.cacciola@gmail.com
Organization:SciSoft
Date:
2005-10-03
Number: N1879=05-0139
An out-of-range conversion occurs when a source value cannot be presented in a destination type, not even approximately between 2 adjacent destination values.
If the destination type is unsigned, the conversion, even if logically out-of-range, if always defined (4.7.2) as the least congruent unsigned value.
If the destination type is a signed integer, the result of an out-of-range conversion is implementation-defined (4.7.3).
If the destination type is floating-point, or the source type is floating-point and the destination type integer, an out-of-range conversion is undefined-behavior (4.8.1,4.9.1).
Clearly, out-of-range conversions are a problem because they can involve from implementation-defined to undefined behavior. Even signed to unsigned conversion can be a problem because the resulting value, even if well defined, can be as wrong as any arbitrary unrelated value.
Numerical applications, ranging from sophisticated scientific software to modeling to accounting to simple number manipulation can rarely disregard the problems associated with out-of-range conversions. In consequence, most such applications incorporate ad-hoc out-of-range checking code and protocols.
Furthermore, most C++ users are unaware of these rules. For instance, most C++ programmers are unaware that an out-of-range floating point conversion is undefined behavior (...and can for instance cause the client's hard disk to be wiped out entirely, as the say goes)
Generic Programming came into C++ along with the need for a numerical conversion utility providing well-defined behavior for all generic conversion cases (all combinations of source and destination types). So much in fact, that the Boost project launched, 6 years ago, with such an utility as part of its initial offering.
Whether boost::numeric_cast<>
or any other in-house facility,
most numerical applications have been using range-checked numeric conversions
from a long time.
But there is a problem: properly determining whether a source value is in range
for a given destination type is tricky because involves comparing a value of a
type with the boundary values of a different type, task that itself requires a
numeric conversion. This is in fact so difficult that boost::numeric_cast<>
had it wrong for certain cases for years, even after a number of revisions.
A numeric_cast<>
is therefore a perfect candidate for
standardization because it is widely needed but its implementation much too
difficult to ask users to roll their own (or need one from a non
standard library)
The Boost
Numeric Conversion Library introduced a re-implementation of boost::numeric_cast<>
which using metaprogramming fixed all the range-checking problems of the
initial version and even gained compile-time optimizations by totally avoiding
the range-check when unnecessary.
That library, which serves as a reference implementation, developed the right concepts that are needed to formulate a proper range-checking logic for any combination of types (that is, a formulation that prevents the conversions needed by the check itself to be out-of-range). It also shown that template-metaprogramming can be used to optimize away the range-checking when unneeded.
The Boost library uses a policy-based design that offers users a wide latitude
of choices (user-defined-types support, custom out-of-range handling, float to
integer rounders, etc), but the facility proposed in this paper for
standardization is a simplified version more in the spirit of the original boost
numeric_cast<>
The proposed numeric_cast<T>(s)
shall give the same result as
static_cast<T>(s)
(that is, the conversion itself is a just
standard conversion) but with an added out-of-range check which implementations
are required to bypass if unneeded (the proposed text defines exactly when is
this the case). The out-of-range check is itself performed separately by
another proposed function: bool is_in_range<T>(s)
which
users can call if they need a domain-specific out-of-range response. If numeric_cast<T>(s)
detects an out of range condition, an out-of-range installable handler is
called.
The proposed text is intended to allow upcoming numeric types like decimal
types, big integers, rationals, etc to be usable with the facility. To that
effect, the proposed text includes some definitions which encompasses these
eventually-standard types. Additionally, the proposed text intends to allow a
C++ program to extend the standard numeric_cast<>
for
user-defined-types.
Nonetheless, even though the facility must interact with other pieces of the standard, the proposal takes the form of a pure extension. There are only additions.
The fundamental need that this proposal aims to satisfy is providing range-checked standard conversions because range checking is a general need in a wide range of application domains. But there are other needs: for example, in converting a floating-point type to an integer type it might be critical to be able to control how the fractional part is handled (instead of simply discarding it as a standard conversion does), or maybe an application needs to trap inexact (but in-range) conversions (when the source value is represented as one of the two adjacent values in the destination type); or "fix" out of range conversions by choosing the closest boundary value in the destination type. Yet from all this features, range checking is the only one that doesn't need to redefine the conversion itself because it can be provided as a precondition to a standard conversion.
It is certainly possible to design a facility to give users further control over the conversion process. To some extent, C99 and the Boost Numeric Conversion Library do that. However, the pieces that would be directly involved and affected are yet to be standardized (additions from C99, additional numeric types, like decimal, integer, rational, etc..) so is more convenient to take it one step at a time and standardize at this point only the simplest solution to the more general problem: range checking.
A policy-based design like that used in the Boost Numeric Conversion library is certainly a reasonable way to give users the extra latitude, but such designs has as a drawback that the resulting behavior is on the hands of the users. For such a fundamental part of a language as its standard library, just carefully specifying the requirements and expected behavior for the policies is simply not enough. There is no discussion about the potential and usefulness of policies, but the actual instrumentation of such designs needs tools from the language, like concepts and design-by-contract forms, which are not yet available (but these have been already formally proposed). Thus, the policy-based design of the Boost version has been dropped in favor of a simpler but better defined facility.
The canonical way to treat exceptional errors in C++ is by throwing an exception. However, numeric conversions are specially important in numeric applications and numeric applications are particularly common in restricted platforms like embedded systems. In consequence, always throwing an exception in the presence of an out-of-range conversion is unlikely to be welcome by the numerical community which is intended to be a primary target. Thus, the proposed solution uses an installable handler (like the new handler) so that implementations and programs can customize the out-of-range response to suit the target platform.
At this point in the C++ standard life-cycle, a number of numeric types are
being proposed. This proposal assumes that at least some of these types will be
accepted so it tries to cover the conversions involving not only arithmetic
types but these eventual standard numeric types as well. Since
the proposed numeric_cast<>()
only adds range checking but
doesn't define the conversion itself, the proposal only has to properly cover
the range checking logic for any type it intends to specify behavior. To
achieve that, the specification of the range checking logic that
implementations must follow is formulated with the assumption that the
implementation has complete access to the details of these standard numeric
types. In particular, the implementation is required to be able to test
whether a boundary value of any given standard numeric type is
representable in any other given standard numeric type.
Furthermore, the proposal adds a simple provision to allow C++ programs to make
their user-defined-types be usable with numeric_cast<>
: they
are allowed to provide additional specializations, even partial (assuming the
language eventually allows that), of the template function.
A. Add the following to 17.4.3.5 [lib.handler.functions]
out_of_range_handler
set_of_out_range_handler
B. Add the following to the synopsis for <numeric> at 26.4, [lib.numeric.ops]
template<class T, class S> bool is_in_range ( S s ) throw() ;template<class T, class S> T numeric_cast ( S s )throw ( range_error )
;
C. Add the following subclauses:
any type which satisfies the numeric type requirements (26.1)
[Note: a user defined type can fit this definition -end note]
any numeric type (17.1.21) defined in this standard, including library provided types such as complex<>
any conversion, standard or user defined, involving numeric types (17.1.21)
any numeric conversion (17.1.23) involving standard numeric types (17.1.22)
standard numeric conversions from any value of a standard numeric type
(17.1.2) S to a value of a standard numeric type T such that mumeric_limits<S>::is_signed!=mumeric_limits<T>::is_signed
standard numeric conversions from any value of a standard numeric type
(17.1.2) S to a value of a standard numeric type T such that mumeric_limits<T>::is_bounded==true
and numeric_limits<S>::lowest()
1
cannot be represented2
in the type T when T is signed or numeric_limits<S>::max()
cannot
be represented2
in the type T (whether signed or not).
That is, conversions for which some values of the source type cannot be
represented2 in
the destination type. If conversions from S to T are not sub-ranged
then all values (or all positive values if T is unsigned) of type
S can be represented 2
in the type T.
18.7.1 Type out_of_range_handler [lib.support.runtime.outofrange]
typedef void (*out_of_range_handler)();
1. The type of the handler function called by numeric_cast<> (26.4.5) when an out of range is detected
2. Required behavior: an out_of_range_handler shall perform one of the following::
- throw an exception of type range_error (19.1.7) or a class derived from range_error
- call either abort() or exit()
- return
3. An out of range handler installed by a program or the implementation is allowed to perform additional operations, with side effects, prior to the required operation, provided they do not incur undefined behavior.
[Note: allowing additional operations is intended to let a C++ program properly register and report the incident; not to attempt to recover from the error -endnote]
out_of_range_handler set_out_of_range_handler( out_of_range_handler new_h ) throw() ;
1. Effects: Establishes the function designated by new_h as the current out_of_range_handler.
2. Returns: 0 on first call, the previous out_of_range_handler on subsequent calls.
template<class T, class S> bool is_in_range ( Source s ) throw() ;
1. Returns:
A. If conversions from S to T are not sub-ranged (17.1.26) and T is a signed type,
true
for any value of s.B. If conversions from S to T are not sub-ranged (17.1.26) and T is an unsigned type, false if
(s < static_cast<S>(0)).
C. If conversions from S to T are sub-ranged (17.1.26) and S is a signed type; the result of evaluating this expression:
(s > static_cast<S>(numeric_limits<T>::lowest()
1- static_cast<S>(1)
&& (s < static_cast<S>(numeric_limits<T>::max () + static_cast<S>(1)).D. If conversions from S to T are sub-ranged (17.1.26) and S is an unsigned type; the result of evaluating this expression:
(s < static_cast<S>(numeric_limits<T>::max() + static_cast<1>).
E. If either S or T are not standard numeric types (17.1.22), an unspecified result.
[Note: Cases A to D apply only when S and T are standard numeric types (17.1.22)]
[Note: In cases C and D, the in-range expressions uses the target but ranges expanded by 1 source unit (and the comparison is for <, not >). This logic is required in order to properly detect in-range floaint-point to integer conversions. The logic happens to works correctly in all other cases so it is used as the single general condition.]
2. A C++ program is allowed to defined specializations of this template function provided that S or T or both are user-defined-types. If both S and T are standard numeric type (17.1.22) the specializations are ill-formed and the implementation is required to issue diagnostic.
template<class T, class S> T numeric_cast( S s ) throw ( range_error );
1. The result of the expression
numeric_cast<T>(s)
is the same as the result of the expressionstatic_cast<T>(s)
with the following added restrictions and side-effects:2. If either S or T are not standard numeric types (17.1.22), the behavior is unspecified if there exist in the program a user-provided specialization of the function for these types and undefined if such a user-provided specialization does not exist.
4. If conversions from S to T are sub-ranged (17.1.26) or anti-signed (17.1.25),
is_in_range<T>(s)
shall be called, and if the result of that isfalse
, the installed out_of_range-handler shall be invoked, if any.5. If conversions from S to T are not sub-ranged (17.1.26)and not anti-signed (17.1.25),
is_in_range<T>(s)
shall not be called and numeric_cast<T>(s) shall be distinguishable from static_cast<T>(s) only in the additional type requirements. That is, implementations are required to skip the overhead of range checking for super-ranged conversions.6. A C++ program is allowed to defined specializations of this template function provided that S or T or both are user-defined-types. If both S and T are standard numeric type (17.1.22) the specialization is ill-formed and the implementation is required to issue a diagnostic.
7. The implementation is allowed to implement
numeric_cast<>
as an internal operator and not as a explicit library function provided it can diagnose a violation to subclause 6 in case a C++ programs adds specializations.
Footnotes:
All the people in the boost community, particularly those involved in the development of the Boost.NumericConversion library.
Thorsten Ottosen for his help preparing this proposal.
Joe Gottman for detecting an error in the range checking logic in the first
revision of this paper.